//all button presses/releases are considered significant, but analog input is limited
//so that the data sets don't become too enormous.  Lowering D_THRESH or T_THRESH will
//cause there to be more resolution in the data (and hence much more data)
final float D_THRESH = 0.01; //difference in analog values to be considered "different"
final int T_THRESH = 10; //milliseconds between accepting new analog values
  

//this is the dataPoint class...
class dataPoint
{
  int CONTROLLER;
  int TIME;
  int[] DIGITAL;
  int[] ANALOG;
  dataPoint(String[] data, int numDigital, int numAnalog)
  {
    CONTROLLER = Integer.parseInt(data[0]);
    TIME = Integer.parseInt(data[1]);
    DIGITAL = new int[numDigital];
    ANALOG = new int[numAnalog];
    for(int i = 0; i < numDigital; i++)
    {
      DIGITAL[i] = Integer.parseInt(data[i+2]);
    }
    for(int i = 0; i < numAnalog; i++)
    {
      ANALOG[i] = Integer.parseInt(data[i+2+numDigital]);
    }
  }
  
  //for constructing dataPoints when recording data... (no need to do string conversions twice...)
  dataPoint(int con, int time, int[] d, int[] a)
  {
    CONTROLLER = con;
    TIME = time;
    DIGITAL = new int[d.length];
    ANALOG = new int[a.length];
    for(int i = 0; i < d.length; i++)
    {
      DIGITAL[i] = d[i];
    }
    for(int i = 0; i < a.length; i++)
    {
      ANALOG[i] = a[i];
    }
  }
  
  //used for comparing datapoints when recording
  boolean isDifferent(dataPoint dp)
  {
    //any button state change is considered a difference
    for(int i = 0; i < DIGITAL.length; i++)
    {
      if (dp.DIGITAL[i] != DIGITAL[i]) return true;
    }
    //analog stick changes within a certain threshold are considered a difference...
    //given that a certain amount of time has passed
    for(int i = 0; i < ANALOG.length; i++)
    {
      if (dp.ANALOG[i] != ANALOG[i]) {
        //println("RETURNING TRUE"); 
        return true;
      }
    }
    //println("Not sufficiently different");
    return false;
  }  
  
  void mPrint()
  {
    String s = CONTROLLER + " " + TIME;
    for(int i = 0; i < DIGITAL.length; i++)
    {
      s += " " + DIGITAL[i];
    }
    for(int i = 0; i < ANALOG.length; i++)
    {
      s += " " + ANALOG[i];
    }
    println(s);
  }
  
  //Do we want the controller? <--getting ready to send it out over UDP
  //or do we want just the data? <--getting ready to export it (and adding a new controller value)
  String getString(boolean con)
  {
    String s = "";
    if(con) s += CONTROLLER + " ";
    s += TIME+"";
    for(int i = 0; i < DIGITAL.length; i++)
    {
      s += " " + DIGITAL[i];
    }
    for(int i = 0; i < ANALOG.length; i++)
    {
      s += " " + ANALOG[i];
    }
    return s;
  }
  
  //use this to get a string which conforms to the specified format
  //if D < DIGITAL.length, just returns D
  //if D > DIGITAL.length, returns 0's for extra values
  String getString(boolean con, int D, int A)
  {
    String s = "";
    if(con) s += CONTROLLER + " ";
    s += TIME+"";
    for(int i = 0; i < D; i++)
    {
      if(i >= DIGITAL.length) s += " " + 0;
      else s += " " + DIGITAL[i];
    }
    for(int i = 0; i < A; i++)
    {
      if(i >= ANALOG.length) s += " " + 127;  //Analog 0
      else s += " " + ANALOG[i];
    }
    return s;
  }
  
  //just grabs the data values (in the specified format)
  //note that there is a space at the beginning of the returned string
  String getData(int D, int A)
  {
    String s = "";
    for(int i = 0; i < D; i++)
    {
      if(i >= DIGITAL.length) s += " " + 0;
      else s += " " + DIGITAL[i];
    }
    for(int i = 0; i < A; i++)
    {
      if(i >= ANALOG.length) s += " " + 127;  //Analog 0
      else s += " " + ANALOG[i];
    }
    return s;   
  }
  
  //gets ALL of the data
  String getData()
  {
    String s = "";
    for(int i = 0; i < DIGITAL.length; i++)
    {
      s += " " + DIGITAL[i];
    }
    for(int i = 0; i < ANALOG.length; i++)
    {
      s += " " + ANALOG[i];
    }
    return s;
  }
}

//this is the class for loading/building a simple input file.
class dataFile
{
  String path;
  boolean isValid = true;
  String name = "";
  dataPoint[] data;
  int numDigital;
  int numAnalog;
  int numControl;
  
  dataFile(String p)
  {
     path = p;
     String[] preData = loadStrings(path);
     try {
       numControl = Integer.parseInt(preData[0]);
       numDigital = Integer.parseInt(preData[1]);
       numAnalog = Integer.parseInt(preData[2]);     
     } catch (NumberFormatException e) {
       isValid = false;
       return;
     }
     //println(numDigital + " " + numAnalog);
     data = new dataPoint[preData.length - 3];
     for(int i = 3; i < preData.length; i++)
     {
       String[] s = Explode.explode(" ",preData[i]);
       data[i-3] = new dataPoint(s,numDigital,numAnalog);
     }
  }
  
  int getDuration() //returns duration in milliseconds
  {
    return(data[data.length-1].TIME - data[0].TIME);
  }
  
  String getName()
  {
      if(name.equals(""))
      {
        return(getFileName());      
      }else{
        return name;      
      }
  }

  String getFileName()
  {
    String[] chunks = Explode.explode("\\",path);
    return(chunks[chunks.length-1]);          
  }  
  
  void setName(String n)
  {
    name = n;
  }
}

class dataInstance
{
  dataFile file;
  String name;
  int numDigital; //how many are active in this instance?
  int numAnalog;  //how many are active in this instance?
  int inPoint;
  int outPoint;
  float dataScale = 1.0;
  float offset;  //where on the timeline should this be placed?
  
  //This variable is only used by the currently-being-dragged data instance
  float clickPct = 0.5;  //default to center
  dataInstance original = null;
  
  //for now, having arrays of channel activations and channel names is fine
  //if i get to the point of having inputs mapped to bones in the editor, it 
  //might make more sense to create a "Channel" class with fields for...
  //-active
  //-name
  //-transform
  //-clamp
  //-bone
  
  boolean[] dChan;
  boolean[] aChan;
  String[] dChanName; 
  String[] aChanName;
  
  dataInstance(dataFile f)
  {
    file = f;
    //until told otherwise, use the name of the file
    name = f.getName();
    
    //until told otherwise, assume you're using all of the file's input channels
    numDigital = file.numDigital;
    numAnalog = file.numAnalog;
    dChan = new boolean[numDigital];
    aChan = new boolean[numAnalog];
    Arrays.fill(dChan,true);
    Arrays.fill(aChan,true);
    dChanName = new String[numDigital];
    aChanName = new String[numAnalog];
    for(int i = 0; i < numDigital; i++)
    {
      dChanName[i] = "Digital "+i;
    }
    for(int i = 0; i < numAnalog; i++)
    {
      aChanName[i] = "Analog "+i;
    }
    
    inPoint = 0;
    outPoint = f.getDuration();
  }
  
  dataInstance copy(int hOver)
  {
    dataInstance d = new dataInstance(file);
    //run through the various fields and copy them over...
    d.name = name;
    d.numDigital = numDigital;
    d.numAnalog = numAnalog;
    for(int i = 0; i < numDigital; i++)
    {
      d.dChan[i] = dChan[i];
      d.dChanName[i] = dChanName[i];
    }
    for(int i = 0; i < numAnalog; i++)
    {
      d.aChan[i] = aChan[i];
      d.aChanName[i] = aChanName[i];
    }
    
    d.inPoint = inPoint;
    d.outPoint = outPoint;   
    d.offset = offset;
    d.dataScale = dataScale;
    
    d.original = this;
    
    d.clickPct = ((float)hOver - (float)offset)/(float)getDuration();
    
    return d;    
  }
  
  //conform takes a dataInstance and makes this dataInstance match it
  void conform(dataInstance d)
  {
    //run through the various fields and copy them over...
    name = d.name;
    numDigital = d.numDigital;
    numAnalog = d.numAnalog;
    for(int i = 0; i < numDigital; i++)
    {
      dChan[i] = d.dChan[i];
      dChanName[i] = d.dChanName[i];
    }
    for(int i = 0; i < numAnalog; i++)
    {
      aChan[i] = d.aChan[i];
      aChanName[i] = d.aChanName[i];
    }
    
    inPoint = d.inPoint;
    outPoint = d.outPoint;   
    offset = d.offset;
    dataScale = d.dataScale;
    
    original = null;
    clickPct = 0.5;
  }
  
  void setName(String s)
  {
    if(name != "") name = s;
  }
  
  boolean mOverlaps(dataInstance d)
  {
    float length1 = getDuration();
    float length2 = d.getDuration();
    
    if(offset + length1 >= d.offset && offset + length1 <= d.offset + length2) return true;
    if(d.offset + length2 >= offset && d.offset + length2 <= offset + length1) return true;
    
    return false;
  }
  
  int getDuration()
  {
    return (int)((outPoint - inPoint)*dataScale);
  }
  
  void setDuration(int howLong)
  {
    setScale((float)howLong / ((float)outPoint - (float)inPoint));
  }
  
  void setScale(float s)
  {
    dataScale = s;
  }
  
  //returns true if the dataInstance contains the specified millisecond timestamp
  boolean contains(float t)
  {
    return (t >= offset && t <= offset + getDuration());
  }
  
  //this function takes an index value and returns the next millisecond value (or -1 if there are no more)
  //it should be noted that if in/out points are set, these count as "virtual" keys which assume whatever
  //the previous/next value would be, respectively
  int getKeyTime(int whichKey)
  {
    if(whichKey == 0) return (int)offset;
    
    int keyCount = 0;
    for(int i = 0; i < file.data.length; i++)
    {
      if(file.data[i].TIME > inPoint)  //found a scoped dataPoint!
      {
        keyCount++;  //skips 0 (handled separately)
        boolean goneOver = false;
        if(file.data[i].TIME < outPoint)
        {
          if(keyCount == whichKey)
          {
            return (int)(offset + (file.data[i].TIME - inPoint)*dataScale);            
          }
        }else{ //too high!
          if(keyCount == whichKey && !goneOver)
          {
            goneOver = true;
            return (int)(offset + (outPoint - inPoint)*dataScale);            
          }
          break;
        }
      }
    }
    
    //if I'm still here, must not have found that key...
    return -1;
  }
  
  String getKeyData(int whichKey)
  {
    if(whichKey == 0) return (int)offset + "" + file.data[0].getData();
    
    String s = "";
    
    int keyCount = 0;
    for(int i = 0; i < file.data.length; i++)
    {
      if(file.data[i].TIME > inPoint)  //found a scoped dataPoint!
      {
        keyCount++;  //skips 0 (handled separately)
        boolean goneOver = false;
        if(file.data[i].TIME < outPoint)
        {
          if(keyCount == whichKey)
          {
            return (int)(offset + (file.data[i].TIME - inPoint)*dataScale) + "" + file.data[i].getData();
          }
        }else{ //too high!
          if(keyCount == whichKey && !goneOver)
          {
            goneOver = true;
            return (int)(offset + (outPoint - inPoint)*dataScale) + "" + file.data[i].getData();            
          }
          break;
        }
      }

    }
    
    //if I'm still here, must not have found that key...
    return "ouch";
  }
}
